---
title: Zero Copy
description:
  Learn how to use Anchor's zero-copy deserialization feature to handle large
  account data in Solana programs.
---

## Overview

Zero-copy deserialization allows programs to read and write account data
**directly from memory** without copying or deserializing it. This is essential
for handling large accounts efficiently on Solana.

### Why Use Zero-Copy?

Traditional account deserialization (`Account<T>`) copies data from the account
into a heap-allocated struct. This has limitations:

- **Size Constraints**: Stack (4KB) and heap (32KB) limits restrict account sizes
- **Compute Cost**: Deserialization consumes significant compute units
- **Memory Overhead**: Data is duplicated in memory

Zero-copy (`AccountLoader<T>`) instead:

- **Direct Access**: Casts raw account bytes to the struct type (no copying)
- **Larger Accounts**: Supports accounts up to 10MB (10,485,760 bytes)
- **Lower Compute**: ~90% reduction in CU usage for large accounts
- **In-Place Updates**: Modifies account data directly

### Performance Comparison

| Account Size | Account\<T\> | AccountLoader\<T\> | Improvement |
| ------------ | ------------ | ------------------ | ----------- |
| 1 KB         | ~8,000 CU    | ~1,500 CU          | 81% faster  |
| 10 KB        | ~50,000 CU   | ~5,000 CU          | 90% faster  |
| 100 KB       | Too large    | ~12,000 CU         | Possible    |
| 1 MB         | Impossible   | ~25,000 CU         | Possible    |

### When to Use Zero-Copy

   **Use zero-copy for:**
- Accounts larger than 1KB
- Arrays with many elements (orderbooks, event queues)
- High-frequency operations
- Compute-sensitive programs
 **Use regular Account\<T\> for:**
- Small accounts (< 1KB)
- Dynamic data structures (Vec, String, HashMap)
- Frequently changing schemas
- Simple state that doesn't need optimization

## Usage

Zero copy is a deserialization feature that allows programs to read account data
directly from memory without copying it. This is particularly useful when
working with large accounts.

To use zero-copy add the `bytemuck` crate to your dependencies. Add the
`min_const_generics` feature to allow working with arrays of any size in your
zero-copy types.

```toml title="Cargo.toml"
[dependencies]
bytemuck = { version = "1.20.0", features = ["min_const_generics"] }
anchor-lang = "0.32.1"
```

### Define a Zero Copy Account

To define an account type that uses zero-copy, annotate the struct with
[`#[account(zero_copy)]`](https://github.com/coral-xyz/anchor/blob/0e5285aecdf410fa0779b7cd09a47f235882c156/lang/attribute/account/src/lib.rs#L417).

```rust
// [!code highlight]
#[account(zero_copy)]
pub struct Data {
    // 10240 bytes - 8 bytes account discriminator
    pub data: [u8; 10232],
}
```

The `#[account(zero_copy)]` attribute automatically implements several traits
required for zero-copy deserialization:

```rust
// [!code highlight:4]
#[derive(Copy, Clone)]
#[derive(bytemuck::Zeroable)]
#[derive(bytemuck::Pod)]
#[repr(C)]
struct Data {
  // --snip--
}
```

### Use AccountLoader for Zero Copy Accounts

To deserialize a zero-copy account, use
[`AccountLoader<'info, T>`](https://github.com/coral-xyz/anchor/blob/0e5285aecdf410fa0779b7cd09a47f235882c156/lang/src/accounts/account_loader.rs#L95-L99),
where `T` is the zero-copy account type defined with the `#[account(zero_copy)]`
attribute.

For example:

```rust
#[derive(Accounts)]
pub struct InstructionAccounts<'info> {
    // [!code word:AccountLoader]
    // [!code highlight]
    pub zero_copy_account: AccountLoader<'info, Data>,
}
```

#### Initialize a Zero Copy Account

The `init` constraint can be used with the `AccountLoader` type to create a
zero-copy account.

```rust
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        // [!code word:init:1]
        // [!code highlight:4]
        init,
        // 10240 bytes is max space to allocate with init constraint
        space = 8 + 10232,
        payer = payer,
    )]
    pub data_account: AccountLoader<'info, Data>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
```

<Callout type="info">
  The `init` constraint is limited to allocating a maximum of 10240 bytes due to
  CPI limitations. Under the hood, the `init` constraint makes a CPI call to the
  SystemProgram to create the account.
</Callout>

When initializing a zero-copy account for the first time, use
[`load_init`](https://github.com/coral-xyz/anchor/blob/0e5285aecdf410fa0779b7cd09a47f235882c156/lang/src/accounts/account_loader.rs#L199-L221)
to get a mutable reference to the account data. The `load_init` method also sets
the account discriminator.

```rust
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // [!code word:load_init]
    // [!code highlight]
    let account = &mut ctx.accounts.data_account.load_init()?;
    account.data = [1; 10232];
    Ok(())
}
```

For accounts that require more than 10240 bytes, use the
[`zero`](https://github.com/coral-xyz/anchor/blob/0e5285aecdf410fa0779b7cd09a47f235882c156/lang/syn/src/codegen/accounts/constraints.rs#L200-L264)
constraint instead of `init`. The `zero` constraint verifies the account has not
been initialized by checking that its discriminator has not been set.

```rust
#[derive(Accounts)]
pub struct Initialize<'info> {
    // [!code word:zero]
    // [!code highlight]
    #[account(zero)]
    pub data_account: AccountLoader<'info, Data>,
}
```

With the `zero` constraint, you'll need to first create the account in a
separate instruction by directly calling the System Program. This allows you to
create accounts up to Solana's maximum account size of 10MB (10_485_760 bytes),
bypassing the CPI limitation.

Just as before, use `load_init` to get a mutable reference to the account data
and set the account discriminator. Since 8 bytes are reserved for the account
discriminator, the maximum data size is 10_485_752 bytes (10MB - 8 bytes).

```rust
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // [!code word:load_init]
    // [!code highlight]
    let account = &mut ctx.accounts.data_account.load_init()?;
    account.data = [1; 10_485_752];
    Ok(())
}
```

#### Update a Zero Copy Account

Use
[`load_mut`](https://github.com/coral-xyz/anchor/blob/0e5285aecdf410fa0779b7cd09a47f235882c156/lang/src/accounts/account_loader.rs#L172-L195)
when you need mutable access to update an existing zero-copy account:

```rust
#[derive(Accounts)]
pub struct Update<'info> {
    // [!code highlight]
    #[account(mut)]
    pub data_account: AccountLoader<'info, Data>,
}
```

```rust
pub fn update(ctx: Context<Update>) -> Result<()> {
    // [!code word:load_mut]
    // [!code highlight]
    let account = &mut ctx.accounts.data_account.load_mut()?;
    account.data = [2; 10232];
    Ok(())
}
```

#### Read a Zero Copy Account

Use
[`load`](https://github.com/coral-xyz/anchor/blob/0e5285aecdf410fa0779b7cd09a47f235882c156/lang/src/accounts/account_loader.rs#L154-L169)
to only read the account data.

```rust
#[derive(Accounts)]
pub struct ReadOnly<'info> {
    pub data_account: AccountLoader<'info, Data>,
}
```

```rust
pub fn read_only(ctx: Context<ReadOnly>) -> Result<()> {
    // [!code word:load]
    // [!code highlight]
    let account = &ctx.accounts.data_account.load()?;
    msg!("First 10 bytes: {:?}", &account.data[..10]);
    Ok(())
}
```

## Common Patterns

### Nested Zero-Copy Types

For types used within zero-copy accounts, use `#[zero_copy]` (without `account`):

```rust
#[account(zero_copy)]
pub struct OrderBook {
    pub market: Pubkey,
    pub bids: [Order; 1000],
    pub asks: [Order; 1000],
}

// [!code word:#[zero_copy]]
#[zero_copy]
pub struct Order {
    pub trader: Pubkey,
    pub price: u64,
    pub quantity: u64,
}
```

### Accessor Methods for Byte Arrays

Zero-copy uses `#[repr(packed)]`, making field references unsafe. Use the
`#[accessor]` attribute for safe getter/setter methods:

```rust
#[account(zero_copy)]
pub struct Config {
    pub authority: Pubkey,
    // [!code word:accessor]
    #[accessor(Pubkey)]
    pub secondary_authority: [u8; 32],
}

// Usage:
let config = &mut ctx.accounts.config.load_mut()?;
let secondary = config.get_secondary_authority();
config.set_secondary_authority(&new_authority);
```

### Zero-Copy with PDAs

Zero-copy accounts work seamlessly with program-derived addresses:

```rust
#[derive(Accounts)]
pub struct CreatePdaAccount<'info> {
    #[account(
        init,
        // [!code word:seeds]
        seeds = [b"data", authority.key().as_ref()],
        bump,
        payer = authority,
        space = 8 + std::mem::size_of::<Data>(),
    )]
    pub data_account: AccountLoader<'info, Data>,
    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}
```

### Separate Types for RPC Parameters

Zero-copy types cannot derive `AnchorSerialize`/`AnchorDeserialize`. Use
separate types for instruction parameters:

```rust
// For zero-copy account
#[zero_copy]
pub struct Event {
    pub from: Pubkey,
    pub data: u64,
}

// For RPC/instruction parameters
// [!code word:AnchorSerialize]
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct EventParams {
    pub from: Pubkey,
    pub data: u64,
}

impl From<EventParams> for Event {
    fn from(params: EventParams) -> Self {
        Event {
            from: params.from,
            data: params.data,
        }
    }
}
```

## Common Pitfalls

### Forgetting the Account Discriminator

Always add 8 bytes for the account discriminator when calculating space:

```rust
// Wrong - missing discriminator
space = std::mem::size_of::<Data>()

//    Correct - includes discriminator
// [!code highlight]
space = 8 + std::mem::size_of::<Data>()
```

### Using Dynamic Types

Zero-copy requires all fields to be `Copy` types:

```rust
#[account(zero_copy)]
pub struct InvalidData {
    pub items: Vec<u64>,  // Vec is not Copy
    pub name: String,     // String is not Copy
}

#[account(zero_copy)]
pub struct ValidData {
    pub items: [u64; 100], //     Fixed-size array
    pub name: [u8; 32],    //     Fixed-size bytes
}
```

### Using load_init vs load_mut

Use `load_init()` for first-time initialization (sets discriminator):

```rust
// First initialization
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
    // [!code highlight]
    let account = &mut ctx.accounts.data_account.load_init()?;
    account.data = [1; 10232];
    Ok(())
}

// Subsequent updates
pub fn update(ctx: Context<Update>) -> Result<()> {
    // [!code highlight]
    let account = &mut ctx.accounts.data_account.load_mut()?;
    account.data = [2; 10232];
    Ok(())
}
```

### Not Validating Array Indices

Always validate array indices to prevent panics:

```rust
pub fn update_item(
    ctx: Context<Update>,
    index: u32,
    value: u64
) -> Result<()> {
    let account = &mut ctx.accounts.data_account.load_mut()?;
    
    // [!code highlight:4]
    require!(
        (index as usize) < account.items.len(),
        ErrorCode::IndexOutOfBounds
    );
    
    account.items[index as usize] = value;
    Ok(())
}
```

## Real-World Use Cases

### Event Queue Pattern

Store large sequences of events efficiently:

```rust
#[account(zero_copy)]
pub struct EventQueue {
    pub head: u64,
    pub count: u64,
    pub events: [Event; 10000],
}

#[zero_copy]
pub struct Event {
    pub timestamp: i64,
    pub user: Pubkey,
    pub event_type: u8,
    pub data: [u8; 32],
}
```

**Used by**: Trading protocols, audit logs, messaging systems

### Order Book Pattern

Efficient storage for trading pairs:

```rust
#[account(zero_copy)]
pub struct OrderBook {
    pub market: Pubkey,
    pub bid_count: u32,
    pub ask_count: u32,
    pub bids: [Order; 1000],
    pub asks: [Order; 1000],
}

#[zero_copy]
pub struct Order {
    pub trader: Pubkey,
    pub price: u64,
    pub size: u64,
    pub timestamp: i64,
}
```

**Used by**: DEXs (Serum, Mango), NFT marketplaces

## Examples

The examples below demonstrate two approaches for initializing zero-copy
accounts in Anchor:

1. Using the `init` constraint to initialize the account in a single instruction
2. Using the `zero` constraint to initialize an account with data greater than
   10240 bytes

### Zero Copy

<Tabs items={["Program", "Client"]}>
<Tab value="Program">

```rust title="lib.rs"
use anchor_lang::prelude::*;

declare_id!("8B7XpDXjPWodpDUWDSzv4q9k73jB5WdNQXZxNBj1hqw1");

#[program]
pub mod zero_copy {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let account = &mut ctx.accounts.data_account.load_init()?;
        account.data = [1; 10232];
        Ok(())
    }

    pub fn update(ctx: Context<Update>) -> Result<()> {
        let account = &mut ctx.accounts.data_account.load_mut()?;
        account.data = [2; 10232];
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        // 10240 bytes is max space to allocate with init constraint
        space = 8 + 10232,
        payer = payer,
    )]
    pub data_account: AccountLoader<'info, Data>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct Update<'info> {
    #[account(mut)]
    pub data_account: AccountLoader<'info, Data>,
}

#[account(zero_copy)]
pub struct Data {
    // 10240 bytes - 8 bytes account discriminator
    pub data: [u8; 10232],
}
```

</Tab>
<Tab value="Client">

```ts title="test.ts"
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ZeroCopy } from "../target/types/zero_copy";

describe("zero-copy", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.ZeroCopy as Program<ZeroCopy>;
  const dataAccount = anchor.web3.Keypair.generate();

  it("Is initialized!", async () => {
    const tx = await program.methods
      .initialize()
      .accounts({
        dataAccount: dataAccount.publicKey,
      })
      .signers([dataAccount])
      .rpc();
    console.log("Your transaction signature", tx);

    const account = await program.account.data.fetch(dataAccount.publicKey);
    console.log("Account", account);
  });

  it("Update!", async () => {
    const tx = await program.methods
      .update()
      .accounts({
        dataAccount: dataAccount.publicKey,
      })
      .rpc();
    console.log("Your transaction signature", tx);

    const account = await program.account.data.fetch(dataAccount.publicKey);
    console.log("Account", account);
  });
});
```

</Tab>
</Tabs>

### Initialize Large Account

When initializing an account that requires more than 10,240 bytes of space, you
must split the initialization into two steps:

1. Create the account in a separate instruction invoking the System Program
2. Initialize the account data in your program instruction

Note that the maximum Solana account size is 10MB (10_485_760 bytes), 8 bytes
are reserved for the account discriminator.

<Tabs items={["Program", "Client"]}>
<Tab value="Program">

```rust title="lib.rs"
use anchor_lang::prelude::*;

declare_id!("CZgWhy3FYPFgKE5v9atSGaiQzbSB7cM38ofwX1XxeCFH");

#[program]
pub mod zero_copy_two {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let account = &mut ctx.accounts.data_account.load_init()?;
        account.data = [1; 10_485_752];
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(zero)]
    pub data_account: AccountLoader<'info, Data>,
}

#[account(zero_copy)]
pub struct Data {
    // 10240 bytes - 8 bytes account discriminator
    pub data: [u8; 10_485_752],
}
```

</Tab>
<Tab value="Client">

```ts title="test.ts"
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ZeroCopyTwo } from "../target/types/zero_copy_two";

describe("zero-copy-two", () => {
  // Configure the client to use the local cluster.
  anchor.setProvider(anchor.AnchorProvider.env());

  const program = anchor.workspace.ZeroCopyTwo as Program<ZeroCopyTwo>;
  const dataAccount = anchor.web3.Keypair.generate();

  it("Is initialized!", async () => {
    const space = 10_485_760; // 10MB max account size
    const lamports =
      await program.provider.connection.getMinimumBalanceForRentExemption(
        space,
      );

    // [!code highlight:7]
    const createAccountInstruction = anchor.web3.SystemProgram.createAccount({
      fromPubkey: program.provider.publicKey,
      newAccountPubkey: dataAccount.publicKey,
      space,
      lamports,
      programId: program.programId,
    });

    // [!code highlight:6]
    const initializeInstruction = await program.methods
      .initialize()
      .accounts({
        dataAccount: dataAccount.publicKey,
      })
      .instruction();

    const transaction = new anchor.web3.Transaction().add(
      createAccountInstruction,
      initializeInstruction,
    );

    const tx = await program.provider.sendAndConfirm(transaction, [
      dataAccount,
    ]);

    console.log("Your transaction signature", tx);

    const account = await program.account.data.fetch(dataAccount.publicKey);
    console.log("Account", account);
  });
});
```

</Tab>
</Tabs>
